#!/usr/bin/python

from decimal import Decimal as D, getcontext

from pylab import plot, ion, show

from math import floor

from time import time, sleep

from logging import log, debug, info, warning, error, critical, exception
import logging

import random

from bisect import bisect

from threading import Thread, Lock

logging.basicConfig(level = logging.DEBUG)

from config import *

class struct:
    pass


class Planet:
    def __init__(self, id = 0,
                 hab = DEFAULT_HAB_RATING,
                 met = DEFAULT_MET_RATING):
        self.hab = hab
        self.met = met

        self.POPULATION_GROWTH = POPULATION_GROWTH
        self.POPULATION_GROWTH *= self.hab ** \
                                max(DOME_CUTOFF, POPULATION_GROWTH_HAB_MODIFIER)

        self.HOUSING_GROWTH = HOUSING_GROWTH
        self.HOUSING_GROWTH *= self.hab ** HOUSING_GROWTH_HAB_MODIFIER

        self.population = INIT_POPULATION

        self.METAL_GROWTH = METAL_GROWTH
        self.METAL_GROWTH *= self.met ** METAL_GROWTH_MET_MODIFIER

        self.FOOD_GROWTH = FOOD_GROWTH
        self.FOOD_GROWTH *= self.hab ** max(DOME_CUTOFF, FOOD_GROWTH_HAB_MODIFIER)

        self.FOOD_CONSUMPTION = FOOD_CONSUMPTION

        # Notice that this is divided, not multiplied
        self.STARVATION_DEATH_RATE = STARVATION_DEATH_RATE
        self.STARVATION_DEATH_RATE /= self.hab ** STARVATION_HAB_MODIFIER

        self.HOMELESS_DEATH_RATE = HOMELESS_DEATH_RATE
        self.HOMELESS_DEATH_RATE /= self.hab ** HOMELESS_HAB_MODIFIER

        self.RESEARCH_GROWTH = RESEARCH_GROWTH

        self.JOB_MOBILITY = JOB_MOBILITY

        self.id = id

        self.lock = Lock()

        self.houses = (self.population / PEOPLE_PER_HOUSE) * INIT_HOUSING

        self.metal = INIT_METAL
        self.food = INIT_FOOD
        self.research = INIT_RESEARCH

        self.employment = DEFAULT_EMPLOYMENT.copy()
        self.goal = DEFAULT_GOALS.copy()

        self.tick_multiplier = 1

        self.curr_tick = 0
        self.record = [(self.population, self.houses, self.food, \
                        self.metal, self.research)]
        info("\nPlanet %s has been created\n", self.id)


    def get_employment_deltas(self):
        deltas = {}
        for key in self.goal.iterkeys():
            deltas[key] = int(round(self.goal[key] * floor(self.population))) - \
                              self.employment[key]
        return deltas

    def get_checked_deltas(self, multiplier = None):
        if multiplier == None:
            multiplier = self.JOB_MOBILITY

        deltas = self.get_employment_deltas()

        for key, value in deltas.iteritems():
            deltas[key] = int(round(float(value) * multiplier))

        # total of all needs to be zero
        total = sum(deltas.itervalues())

        if not total == 0:
            temp = -1
            if total < 0:
                temp = 1

            for x in xrange(abs(total)):
                deltas[KEY_LIST[x % len(KEY_LIST)]] += temp

        # mission accomplished, sum is zero here
        return deltas

    def do_employment_shift(self):
        deltas = self.get_checked_deltas(JOB_MOBILITY)

        # add the deltas in, and if there are any negative employees,
        # turn it back to zero and save our fudge factor (we need to remove
        # an equal number of employees from positive industries so that we
        # retain the sum(self.employment.itervalues()) == int(self.population)
        # invariant)
        fudge = 0
        for key, value in deltas.iteritems():
            self.employment[key] += value
            if self.employment[key] < 0:
                fudge += abs(self.employment[key])
                self.employment[key] = 0

        while fudge:
            key = random.choice(KEY_LIST)
            if self.employment[key] > 0:
                self.employment[key] -= 1
                fudge -= 1
        

    def add_population(self, people):
        assert(people >= 0)

        # fractional people only exist in self.population, so we need to know
        # when a new full person has been added, to give them a job

        old = floor(self.population)
        self.population +=  people
        new_employees = int(self.population - old)

        if new_employees <= 0:
            return

        deltas = self.get_employment_deltas()

        underemployed = []

        for key, value in deltas.iteritems():
            if value > 0:
                underemployed.append((key, value))
        
        to_add = new_employees / 8 + 1

        while new_employees:
            temp = min(to_add, new_employees)
            new_employees -= temp
            self.employment[random.choice(KEY_LIST)] += temp

    def itersum(self, items, sigma = 0.0):
        for item in items:
            sigma += item
            yield sigma


    def kill_population(self, people):
        assert(people >= 0)
        # fractional people only exist in self.population, so we need to know
        # when a full person has been killed to remove them from employment
        old = floor(self.population)
        self.population -= people
        
        death_count = int(old - floor(self.population))
        kill_at_once = death_count / 10 + 1
        killed = 0

        if death_count == 0:
            return

        if int(self.population) == 0:
            self.population = 0.0
            return

        weights = [float(self.employment[key]) / floor(self.population) \
                    for key in KEY_LIST]

        accumulated_weights = list(self.itersum(weights))

        total_weight = accumulated_weights[-1]

        temp_key = None
        while killed < death_count:
            temp_key = KEY_LIST[bisect(accumulated_weights, \
                                    random.random() * total_weight)]
            self.employment[temp_key] -= kill_at_once
            killed += kill_at_once

            if self.employment[temp_key] < 0:
                killed += self.employment[temp_key]
                self.employment[temp_key] = 0

        self.employment[temp_key] += killed - death_count


    def assert_invariants(self):
        assert(self.population >= 0)
        assert(self.metal >= 0)
        assert(self.food >= 0)
        assert(self.houses >= 0)
        assert(self.research >= 0)
        assert(int(self.population) == sum(self.employment.itervalues()))
        assert(abs(1.0 - sum(self.goal.itervalues()) < E))
        for x in self.employment.itervalues():
             assert(x >= 0)
        for x in self.goal.itervalues():
            assert(x > -1 * E)

    def tick(self, num_ticks = 1):
        assert(num_ticks >= 0)
        assert(self.tick_multiplier >= 1)

        for i in xrange(num_ticks * self.tick_multiplier):
            self.food += self.employment['food'] * self.FOOD_GROWTH
            self.food -= self.population * self.FOOD_CONSUMPTION

            if self.food < 0:
                self.kill_population(self.STARVATION_DEATH_RATE * \
                                      -1 * self.food / self.FOOD_CONSUMPTION)
                self.food = 0


            self.metal += self.employment['mining'] * self.METAL_GROWTH
            
            self.research += self.employment['research'] * self.RESEARCH_GROWTH

            self.curr_tick = self.curr_tick + 1

            # TODO: combine all add_population calls, this call is expensive!
            self.add_population(self.population * self.POPULATION_GROWTH)
            self.do_employment_shift()

            self.houses += self.employment['housing'] * self.HOUSING_GROWTH
            housing_surplus = floor(self.houses) - floor(self.population) / PEOPLE_PER_HOUSE

            if housing_surplus >= 0:
                surplus_ratio = housing_surplus / floor(self.houses)
                surplus_ratio *= HOUSING_POP_BONUS_MULTIPLIER
                surplus_ratio = surplus_ratio ** HOUSING_POP_BONUS_EXPONENT
                surplus_ratio = min(surplus_ratio, HOUSING_POP_BONUS_MAX)
                self.add_population(self.population * \
                        self.POPULATION_GROWTH * surplus_ratio)
            else:
                homeless = -1 * housing_surplus * PEOPLE_PER_HOUSE
                self.kill_population(self.HOMELESS_DEATH_RATE * homeless)

            self.assert_invariants()

            self.record.append((self.population, self.houses, self.food, \
                                self.metal, self.research))


    def plot(self):
        plot([x for (x,y,z,w,a) in self.record])
        show()

    def __str__(self):
        return """\
Planet:       %s
Habitability: %s
Metal Rating: %s

Tick:         %s
Population:   %s
Houses:       %s
Food:         %s
Metal:        %s
Research:     %s

Employment:
Housing:            %s people (%s%%)
Mining:             %s people (%s%%)
Food:               %s people (%s%%)
Ships:              %s people (%s%%)
Troops:             %s people (%s%%)
Research:           %s people (%s%%)

Health:             %s people (%s%%)
Police:             %s people (%s%%)
Military:           %s people (%s%%)
Infrastructure:     %s people (%s%%)
Entertainment:      %s people (%s%%)
Unemployment:       %s people (%s%%)\
""" % (self.id, self.habitability_rating, self.metal_rating, self.curr_tick,\
       self.population, self.houses, self.food, self.metal, self.research,\
       self.employment['housing'], self.goal['housing'] * 100.0,\
       self.employment['mining'], self.goal['mining'] * 100.0,\
       self.employment['food'], self.goal['food'] * 100.0,\
       self.employment['ships'], self.goal['ships'] * 100.0,\
       self.employment['troops'], self.goal['troops'] * 100.0,\
       self.employment['research'], self.goal['research'] * 100.0,\
       self.employment['health'], self.goal['health'] * 100.0,\
       self.employment['police'], self.goal['police'] * 100.0,\
       self.employment['military'], self.goal['military'] * 100.0,\
       self.employment['infrastructure'], self.goal['infrastructure'] * 100.0,\
       self.employment['entertainment'], self.goal['entertainment'] * 100.0,\
       self.employment['unemployment'], self.goal['unemployment'] * 100.0)















class PlanetManager(Thread):
    def __init__(self, time_per_tick = 12):
        Thread.__init__(self)
        self.next_id = 10000
        self.planets = {}
        self.curr_tick = 0
        self.time_per_tick = time_per_tick

        self.lock = Lock()
        self.stop_flag = False

        self.setDaemon(True)

    def add_planet(self):
        planet = Planet(self.next_id)
        self.lock.acquire()
        self.planets[self.next_id] = planet
        self.lock.release()
        self.next_id += 1

        return planet

    def has_planet(self, id):
        try:
            self.planets[int(id)]
        except KeyError:
            return False
        except ValueError:
            return False
        
        return True

    def get_planet(self, id):
        # TODO: figure out if this needs to acquire the lock
        return self.planets[int(id)]

    def tick(self):
        self.lock.acquire()
        for planet in self.planets.itervalues():
            planet.lock.acquire()
            planet.tick()
            planet.lock.release()
        self.lock.release()

        self.curr_tick += 1

    def run(self):
        self.next_time = time() + self.time_per_tick
        while not self.stop_flag:
            sleep_time = self.next_time - time()
            self.next_time += self.time_per_tick
            if sleep_time > 0:
                sleep(sleep_time)

            self.tick()


    def stop(self):
        self.stop_flag = True



      
      
